Skip to content

Conversation

@matts1
Copy link
Contributor

@matts1 matts1 commented Oct 18, 2025

Fixes #3303 and #1595

This completely changes the workflow for in-repo configuration. Instead of being stored in a file on disk, we store the configuration wrapped with a signature in order to determine who wrote the configuration.

In the event that the config is considered insecure, we warn the user and require them to run jj config edit --repo/workspace to re-enable it (test_config_security.rs simulates these workflows and is relatively well documented).

If applicable:

  • I have updated CHANGELOG.md
  • I have updated the documentation (README.md, docs/, demos/)
  • I have updated the config schema (cli/src/config-schema.json)
  • I have added/updated tests to cover my changes

@matts1 matts1 force-pushed the push-slpsoluonwkm branch 5 times, most recently from 1ab950a to 2b1e682 Compare October 18, 2025 04:53
@matts1 matts1 marked this pull request as ready for review October 18, 2025 04:54
@matts1 matts1 requested a review from a team as a code owner October 18, 2025 04:54
@matts1
Copy link
Contributor Author

matts1 commented Oct 18, 2025

Notes for reviewers to make your life easier. I recommend reviewing in the following order:

  1. Start with test_config_security.rs to see the UX
  2. Then go to user_config.proto then user_config.rs to see the API
  3. Then look at cli/config.rs to view the implementation

Everything else is just mostly boilerplate changes with little substance.

@matts1 matts1 force-pushed the push-slpsoluonwkm branch from 2b1e682 to c0bed88 Compare October 18, 2025 05:29
/// Sets the directory for the workspace and the workspace-specific config
/// file.
pub fn reset_workspace_path(&mut self, path: &Path) {
pub fn reset_workspace_path(&mut self, ui: &Ui, path: &Path) -> Result<(), CommandError> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if this change doesn't land before workspace config is released there should be a migration like for repo config here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I figured that even if it was released by then it'd be so recently released that my guess is that only you, as the PR author, would probably be the only person who has workspace config.

I can do this if need be, but I'm not convinced it's worth the effort.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't read the code yet, but if workspace-config.toml complicates the stuff, I think it's okay to remove the workspace config layer at all. Maybe we can add --when.workspace_names = ["default"] if --when.workspaces = ["/path/to/workspace"] is tedious to write.

Copy link
Contributor Author

@matts1 matts1 Oct 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if it's an option, I'd prefer we temporarily disable workspace configs for the next release if this code hasn't landed by then, to avoid introducing more tech debt. I think it's probably ok to wait one extra release for the per-workspace configs.

I don't have issues with per-workspace configs, just don't want to have to go through the same migration process for them that we do for per-repo configs to migrate to this new format.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

temporarily disabling or requiring manual migration seems fine to me.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think anyone is saying that workspace config isn't useful. We can all agree that we want the feature enabled, I just think we should delay the release of the feature until this PR lands.

My main concern with leaving it in for the current release and adding migration code is that it adds another corner case that needs to be handled for a very long time, so unless we consider the feature urgent, I think it's worth delaying.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I personally consider that workspace config can be replaced with repo config + conditionals. I don't mean the workspace config is useless, but I don't think it would be worth the effort/complexity to add two separate config + signing file pairs. (It would be nice if we didn't have repo/workspace config files that could be a security risk, but removing all of them wouldn't be acceptable.)

If we're unsure whether we'll re-add workspace config, temporary removal would be good. If we're sure to support workspace config, I think manual migration (without temporary removal) is good enough.


Another concern is that the priority of repo-managed/repo/workspace configs is a bit weird. Since repo-managed config is actually selected per workspace (or working-copy commit), it could be considered a workspace-level config.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I personally consider that workspace config can be replaced with repo config + conditionals.

I don't think this is true for all the people who want to run their coding agents in separately configured workspaces. Since they want a one-time setup for the agents environment.

I don't mean the workspace config is useless, but I don't think it would be worth the effort/complexity to add two separate config + signing file pairs. (It would be nice if we didn't have repo/workspace config files that could be a security risk, but removing all of them wouldn't be acceptable.)

Agreed, having two signed pairs kinda complicates all of this.

If we're sure to support workspace config, I think manual migration (without temporary removal) is good enough.

I also agree with that. Although having an automated way would improve the user experience.

Another concern is that the priority of repo-managed/repo/workspace configs is a bit weird. Since repo-managed config is actually selected per workspace (or working-copy commit), it could be considered a workspace-level config.

Isn't this technically a bug? Because otherwise this seems to be a really bad oversight.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another concern is that the priority of repo-managed/repo/workspace configs is a bit weird. Since repo-managed config is actually selected per workspace (or working-copy commit), it could be considered a workspace-level config.

Isn't this technically a bug? Because otherwise this seems to be a really bad oversight.

I mean it's not obvious whether the repo-managed config should be considered a repo-level or not. If it were "workspace-managed", user's .jj/repo/config.toml would be overridden by workspace-managed config. This wouldn't be what the user would want (unless they use workspace-config.toml extensively.)

Copy link
Contributor Author

@matts1 matts1 Oct 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've actually been having discussions with my team about managed repo config, and they raised some very good points that made me completely reconsider how it should work. Specifically, we have a fake "repo-managed" config currently which is just a symlink to an in-repo config file in chromium.

The issue they've been running into is that they've been doing things like jj new <LTS branch> to cherry-pick certain fixes onto said branch. When they do so, because the LTS branch is old, they don't have any configuration. This also results in very confusing UX when one workspace has one version of the jj config checked out while another workspace has a different version. In general, the current approach:

  • Increases implementation complexity
  • Increases complexity from a user's perspective
    • They might get it out of sync whenever they run jj new
    • They could have one alias work when you're in one place, then suddenly disappear in a different place.

The approach we think would work better is actually to just always read the repo-managed config from trunk(). AFAICT the pros and cons are:

  • Pro: Much simpler to implement
  • Pro: Much simpler from a user's perspective
  • Pro: Consistent user experience between versions
  • Cannot test new configurations easily, since it reads only submitted configs
    • Can be mitigated by adding a global flag to read the config from @ instead. This would bypass trust, and thus mean that they don't have to review their own configs that they wrote.
  • Pro: Any references to out-of-repo tools will actually work with old versions
  • Con: Any references to in-repo tools may break if you move the tool
    • The same applies if the tool didn't exist at an old version, but the previous approach also wouldn't solve that problem

AFAICT, the only downside can be relatively easily mitigated by just not moving tools, or moving tools but adding symlinks, etc.

The priority list would thus be:

  • User
  • Repo-managed
  • Repo
  • Workspace

@matts1 matts1 force-pushed the push-slpsoluonwkm branch from c0bed88 to ea2365d Compare October 19, 2025 03:26
@matts1 matts1 requested a review from adamse October 19, 2025 03:26
@matts1 matts1 force-pushed the push-slpsoluonwkm branch from ea2365d to dfe3864 Compare October 19, 2025 03:47
@matts1 matts1 requested a review from yuja October 19, 2025 08:27
Updated tags will be exported to Git as lightweight tags.

* New commit template keywords `local`/`remote_tags` to show only local/remote
tags. These keywords may be useful in non-colocated Git repositories where
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

further up i mentioned the workspace-config.toml, should we update that too?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll leave this open, depends on whether we land this before a new release.

/// Sets the directory for the workspace and the workspace-specific config
/// file.
pub fn reset_workspace_path(&mut self, path: &Path) {
pub fn reset_workspace_path(&mut self, ui: &Ui, path: &Path) -> Result<(), CommandError> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

workspace config has the upside that you don't have to write absolute paths compared to --when.workspaces. to me having the workspace config seems like a useful feature, but i'm not too fussed as my usecases are also covered by when (but i'm not sure about the other people who were pushing for workspace config).

do we think it's likely this pr doesn't land soon? do we think workspace config is not a useful feature?

if workspace config is good and this pr doesn't land soon, we can mention it as experimental in the release notes and require manual migration once this pr lands?

Copy link
Contributor

@PhilipMetzger PhilipMetzger left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's a bunch of stuff and a opinion

/// Sets the directory for the workspace and the workspace-specific config
/// file.
pub fn reset_workspace_path(&mut self, path: &Path) {
pub fn reset_workspace_path(&mut self, ui: &Ui, path: &Path) -> Result<(), CommandError> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we think workspace config is not a useful feature?

If it was requested by multiple users for multiple use-cases it should stand on its own as a useful feature (which it was). I mean @HybridEidolon's initial use-case was a good example of it uses. I also don't like disabling it when the migration provided here doesn't consider it yet.

do we think it's likely this pr doesn't land soon?

I think this needs some further discussion and I don't expect this go in faster than other stuff still pending.

Fixes jj-vcs#3303 and jj-vcs#1595

This completely changes the workflow for in-repo configuration. Instead
of being stored in a file on disk, we store the configuration wrapped
with a signature in order to determine who wrote the configuration.

In the event that the config is considered insecure, we warn the user
and require them to run `jj config edit --repo/workspace` to re-enable
it (test_config_security.rs simulates these workflows and is relatively
well documented).
This seems like the only feasible option. On nix, apparently we don't
have our own place to write to.
@matts1 matts1 force-pushed the push-slpsoluonwkm branch from dfe3864 to cac1c1a Compare October 20, 2025 03:18
Copy link
Contributor

@PhilipMetzger PhilipMetzger left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another round, sorry for not replying to your question yesterday.

// If we did not provide this grace period, repo configs would be
// disabled for every repo created by an older version of jj.
// TODO: After the grace period is over (~jj 0.47), make this act the
// same as InvalidSignature or RepoMovedError .
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: remove the whitespace before the period

/// Sets the directory for the workspace and the workspace-specific config
/// file.
pub fn reset_workspace_path(&mut self, path: &Path) {
pub fn reset_workspace_path(&mut self, ui: &Ui, path: &Path) -> Result<(), CommandError> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I personally consider that workspace config can be replaced with repo config + conditionals.

I don't think this is true for all the people who want to run their coding agents in separately configured workspaces. Since they want a one-time setup for the agents environment.

I don't mean the workspace config is useless, but I don't think it would be worth the effort/complexity to add two separate config + signing file pairs. (It would be nice if we didn't have repo/workspace config files that could be a security risk, but removing all of them wouldn't be acceptable.)

Agreed, having two signed pairs kinda complicates all of this.

If we're sure to support workspace config, I think manual migration (without temporary removal) is good enough.

I also agree with that. Although having an automated way would improve the user experience.

Another concern is that the priority of repo-managed/repo/workspace configs is a bit weird. Since repo-managed config is actually selected per workspace (or working-copy commit), it could be considered a workspace-level config.

Isn't this technically a bug? Because otherwise this seems to be a really bad oversight.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: squash this and the nix commit into the first one (although you may need a Nix expert to help you with the problems there before doing that).

Comment on lines 553 to 557
let legacy_config = path.join("config.toml");
match std::fs::read_to_string(&legacy_config).context(&legacy_config) {
Ok(content) => {
self.set_repo_config(&content, true)?;
std::fs::remove_file(&legacy_config).context(&legacy_config)?;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean new config is no longer a plain text file? I thought there would be config.toml which is still readable, and a separate file containing signature.

Deleting config.toml would break forward compatibility, so it would require grace period. Even after the grace period, I prefer plain text format because the file can be inspected/edited by using ordinary tools. For example, we can use less .jj/repo/config.toml to review someone else's repository configuration without trusting it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean new config is no longer a plain text file? I thought there would be config.toml which is still readable, and a separate file containing signature.

That's correct. I thought long and hard about this, and flip-flopped a few times, but decided that this was my preferred approach.

Deleting config.toml would break forward compatibility, so it would require grace period.

I have already done this. It automatically migrates from the old config to the new one.

Even after the grace period, I prefer plain text format because the file can be inspected/edited by using ordinary tools. For example, we can use less .jj/repo/config.toml to review someone else's repository configuration without trusting it.

You can run jj config edit --repo to review someone else's repository config without trusting it (since their repo config won't be enabled initially). And if you don't trust it, you delete the parts you don't like (or the whole thing) so it's safe.

I'm not personally a huge fan of the idea of having config files on disk at all, and I actually significantly prefer the new UX. If we train users to "view / edit your config file by editing .jj/repo/config.toml", that will provide a very inconsistent experience.

  • If a user has multiple workspaces, this only works from the main workspace
  • Technically, it is not stored at .jj/repo/config.toml. It is stored at $REPO_DIR/config.toml, and $REPO_DIR is usually .jj/repo. If the repo dir for a particular repo is stored at a different location, it will not work.
  • This doesn't work from subdirectories of the repo root. This usually isn't too much of a problem, but for users at google working on our monorepo, for example, the .jj directory isn't where most users would expect (it's actually at ../.jj/repo)
  • The user now has two ways of editing the config file (jj config edit --repo still works).

IMO, since jj config edit --repo is so much more reliable, I much prefer the consistency of:

  • To edit repository config, run jj config edit --repo
  • To read repository config, run jj config edit --repo
    • We could potentially add jj config show --repo down the line, similar to jj file show if we want to be able to interface with standard tools to read the config, but I simply don't see a strong use case for it. IMO, tools should never need to interface directly with the config files given the existence of jj config get/set/unset/list

The only possible advantage I can think of to keep the file around and edit it directly is that this would allow you to edit the config file in an IDE easily instead of in the terminal.

The thing I don't like about keeping the config file is that by allowing the user to edit the file directly, we are unable to sign the contents of the file. We need a mechanism to tie the signature to the file content, which means jj needs to manage the writing of the content. My personal preference is to keep it as is, as the user can still inspect the config file via jj config edit --repo, and we could easily add jj config show --repo which would write it to disk.

I can see a few different options to solve this problem:

Option 1

We keep the config files, but don't read from them and make them readonly. When you run jj config edit --repo, it would write back the contents to disk as a readonly file.

In the event that the secure config doesn't exist, we simply write the current config to the secure config, then copy it back to the old config path as readonly (this is the migration path).

I don't like anything about this TBH, I don't think it improves on anyway over the existing mechanism

Option 2

Rather than attempting to sign the contents of the config file, we attempt to sign whether we want to enable the config file. We change the proto to:

message RepoConfig {
  bool enable_repo_config = 1;
}

message WorkspaceConfig {
  bool enable_workspace_config = 1;
}

It's not as secure as my current approach, but I believe it would still solve the specific attack vector that is the zip file problem. I think that if we were to change approach, this is the one we should switch to.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to go way out of the way to special case this file, which I thought went against this project's policies.
I don't quite see how you have a config file in the repo without having a file. Wouldn't all the commands that deal with files have to special case it?

Copy link
Contributor Author

@matts1 matts1 Oct 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't quite see how you have a config file in the repo without having a file.

You have a file, it's just that the file, roughly speaking, instead of being a string representing the configuration, contains something more akin to the struct:

struct SecureConfig {
  config: String,
  // sign(config, my credentials)
  signature: Vec<u8>
}

So it is stored in a file, it's just that the file isn't directly edited via a text editor, any modifications are wrapped by jj config edit

Wouldn't all the commands that deal with files have to special case it?

No, because this isn't stored in the repo. No commands read / write the config file (other than jj config, which I've already done in this PR)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, because this isn't stored in the repo.

I thought storing the config in the repo was the whole point. We talked before about switching to different commits that didn't have it, and it was never a file in the repo?

Copy link
Contributor Author

@matts1 matts1 Oct 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought storing the config in the repo was the whole point. We talked before about switching to different commits that didn't have it, and it was never a file in the repo?

It's stored in the repo dir, which is $REPO/.jj/repo/config. That directory is "in the repo" in the sense that it is contained within the directory that is the repository, but it is not tracked by version control, and is thus unaffected by jj new, for example.

It sounds like you may also be mixing this up with a seperate discussion about what I've come to call "managed repo config", which is stored in $REPO/.config/jj/config.toml and thus stored in version control. That is mostly unrelated to this particular discussion.

Comment on lines +71 to +92
pub trait ConfigType: prost::Message + Default + Clone + Debug {
/// The name of this type.
fn kind() -> &'static str;
/// The filename of the configuration file to write.
fn filename() -> &'static str;
/// The string containing the config.
fn config(&self) -> &str;
}

impl ConfigType for RepoConfig {
fn kind() -> &'static str {
"repo"
}

fn filename() -> &'static str {
"config.binpb"
}

fn config(&self) -> &str {
&self.config
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Perhaps, this doesn't have to be abstracted as a trait. Maybe we can use parameter struct something like struct { kind: &'static str, filename: &'static str }?

Comment on lines +146 to +147
let signing_key = key_dir.join("ed25519.der");
let verifying_key = key_dir.join("ed25519.der.pub");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need public-key cryptography to sign and verify user's private data by his key. What we need is something like hash(private_key + path + config) but is considered secure. I think some MAC algorithm can be used. RustCrypto lists hmac crate in this category.

https://en.wikipedia.org/wiki/Message_authentication_code
https://github.com/RustCrypto

I'm not an expert, but pubkey-based algorithm would be computationally more expensive than symmetric-key algorithms or cryptographic hashing.

Copy link
Contributor Author

@matts1 matts1 Oct 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I double-checked with a security expert, they say that you're right and that asymmetric keys aren't improving anything.

They also mentioned that specifically, "HMAC_SHA256 would be a sensible choice"

Comment on lines +141 to +145
let strategy = match etcetera::choose_base_strategy() {
Ok(s) => s,
Err(_) => return Err(UserConfigError::HomeDirNotFound),
};
let key_dir = strategy.data_dir().join("jj/config/keys");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Perhaps, path resolution (and/or key loading) should be done by non-jj-lib code, and provided as a function parameter? It will help write library tests.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement repo ownership checks

5 participants